<?php
namespace App\DataBase;

use App\Exception\SqlException;

abstract class Model implements \ArrayAccess, \Iterator, \JsonSerializable, \Countable
{
    protected $_rawData = [];
    protected $_dirtyProperties = [];

    use ArrayAccessTrait, DataObjectTrait, IteratorTrait, JsonSerializableTrait;

    protected $splitSuffix = '';

    private $_table = '';
    private $_configName = 'default';

    /**
     * @var string 数据库对应的主键字段名称，非Model对象的属性名称
     */
    private $_primaryKeyName = 'id';

    /**
     * @var array [property_name => column_name]
     */
    private $_property2columnMapping = [];
    /**
     * @var array [column_name => property_name]
     */
    private $_column2PropertyMapping = [];

    public function __construct($splitSuffix = '')
    {
        $this->splitSuffix = $splitSuffix;
        $mapping = static::getMapping($splitSuffix);
        if (!isset($mapping['table']) || !isset($mapping['key']) || !isset($mapping['columns'])) {
            throw new \Exception('static method getMapping($splitSuffix) return data error.');
        }
        $this->_table = $mapping['table'];
        $this->_primaryKeyName = $mapping['key'];
        $this->_property2columnMapping = $mapping['columns'];
        $this->column2Property();
        $this->_configName = $this->getPDOName(false);
    }

    /**
     * 初始化数据
     *
     * @param array $data
     */
    public function initRawData($data)
    {
        $this->_rawData = $data;
    }

    /**
     * 获取数据表的字段映射关系
     *
     * @param string $splitSuffix
     * @return array
     * @throws \Exception
     */
    public static function getMapping($splitSuffix = '')
    {
        throw new \Exception('Please implement static method getMapping($splitSuffix).');
    }

    /**
     * 获取PDO的配置名称
     *
     * @param $isForceMaster
     * @return string
     * @throws \Exception
     */
    private function getPDOName($isForceMaster)
    {
        if ($isForceMaster) {
            return $this->getPDOMasterName();
        }
        return $this->getPDOSlaveName();
    }

    /**
     * 强制使用主库
     */
    protected function forceMaster()
    {
        $this->_configName = $this->getPDOName(true);
    }

    /**
     * 获取PDO的Slave配置名称
     *
     * @return string
     * @throws \Exception
     */
    abstract protected function getPDOSlaveName();

    /**
     * 获取PDO的Master配置名称
     *
     * @return string
     * @throws \Exception
     */
    abstract protected function getPDOMasterName();

    /**
     * 创建Model对象
     *
     * @param array $data
     * @param string $splitSuffix
     *
     * @return Model
     */
    public static function create($data, $splitSuffix = '')
    {
        $model = new static($splitSuffix);
        $model->_rawData = $data;
        $model->_dirtyProperties = $data;
        return $model;
    }

    /**
     * 保存数据
     *  如果数据中包含主键那么使用Update,否则使用insert
     * update 返回受影响的记录数
     * insert 返回 last_insert_id
     *
     * @param array $data
     * @return int
     */
    public function save($data = [])
    {
        $dataAccessor = $this->initAccessor(true);
        $data = array_merge($this->_dirtyProperties, $data);
        $primaryKeyValue = null;
        foreach ($data as $property => $value) {
            if (!$this->checkFieldExists($property)) {
                continue;
            }

            $field = $this->_property2columnMapping[$property];
            if ($field == $this->_primaryKeyName) {
                $primaryKeyValue = $value;
                continue;
            }
            $dataAccessor->setField($field, $value);
        }

        if ($primaryKeyValue) {
            $dataAccessor->filter($this->_primaryKeyName, $primaryKeyValue);
            return $dataAccessor->update();
        }
        $dataAccessor->insert();
        $primaryKeyValue = $dataAccessor->lastInsertId();
        $this->{$this->_column2PropertyMapping[$this->_primaryKeyName]} = $primaryKeyValue;
        return $primaryKeyValue;
    }

    /**
     * 执行数据库查询
     *
     * @param array $parameter
     * @return ModelSet
     * @throws SqlException
     */
    public function findAll($parameter = [])
    {
        $dataAccessor = $this->initAccessor();
        if (isset($parameter['property'])) {
            $properties = explode(',', $parameter['property']);
            $mapped = [];
            foreach ($properties as $property) {
                if (isset($this->_property2columnMapping[$property])) {
                    $mapped[] = $this->_property2columnMapping[$property];
                }
            }
            $dataAccessor->loadField($mapped);
        }
        if (isset($parameter['condition'])) {
            $classRef = new \ReflectionClass('\App\DataBase\MysqlAccessor');
            foreach ($parameter['condition'] as $key => $value) {
                $filter = explode('|', $key);
                $property = $filter[0];
                $field = $this->getFieldName($property);
                if (!$field) {
                    continue;
                }
                $op = $classRef->getConstant(strtoupper('OP_' . (isset($filter[1]) ? $filter[1] : (is_array($value) ? 'IN' : 'EQ'))));
                if ($op === false) {
                    throw new SqlException('Undefined OP ' . $key, 90008);
                }

                $dataAccessor->filterWithOperator($field, $value, $op);
            }
        }
        if (isset($parameter['limit'])) {
            $limits = explode(',', $parameter['limit']);
            if (count($limits) == 2) {
                $dataAccessor->limit($limits[0], $limits[1]);
            } else {
                $dataAccessor->limit($limits[0]);
            }
        }
        if (isset($parameter['order']) && is_array($parameter['order'])) {
            foreach ($parameter['order'] as $property => $sortType) {
                $field = $this->getFieldName($property);
                $dataAccessor->sort($field, $sortType);
            }
        }
        $result = $dataAccessor->executeQuery();
        return new ModelSet(get_class(), $result, $this->splitSuffix);
    }

    /**
     * 返回第一条数据
     *
     * @param array $parameter
     *
     * @return Model|null
     * @throws SqlException
     */
    public function findFirst($parameter = [])
    {
        unset($parameter['limit']);
        $parameter['limit'] = 1;
        $set = $this->findAll($parameter);
        return $set->current();
    }

    /**
     * 根据主键更新记录
     *
     * @param array $properties
     * @return int
     * @throws \Exception
     */
    public function update($properties = [])
    {
        $dirtyProperties = array_merge($this->_dirtyProperties, $properties);
        $dataAccessor = $this->initAccessor(true);
        foreach ($dirtyProperties as $property => $value) {
            if (!isset($this->_property2columnMapping[$property])) {
                continue;
            }

            $field = $this->_property2columnMapping[$property];
            if ($field == $this->_primaryKeyName) {
                continue;
            }
            $dataAccessor->setField($field, $value);
        }
        $primaryKeyValue = $this->{$this->_property2columnMapping[$this->_primaryKeyName]};
        if (!$primaryKeyValue) {
            throw new SqlException('Primary key value error', 90009);
        }
        $dataAccessor->filter($this->_primaryKeyName, $primaryKeyValue);
        return $dataAccessor->update();
    }

    /**
     * 根据主键删除记录
     *
     * @return int
     * @throws \Exception
     */
    public function delete()
    {
        $dataAccessor = $this->initAccessor(true);
        $primaryKeyValue = $this->{$this->_property2columnMapping[$this->_primaryKeyName]};
        if (!$primaryKeyValue) {
            throw new SqlException('Primary key value error', 90009);
        }
        $dataAccessor->filter($this->_primaryKeyName, $primaryKeyValue);
        return $dataAccessor->delete();
    }

    /**
     * 初始化 MysqlAccessor
     *
     * @param bool $isForceMaster
     * @return MysqlAccessor
     */
    protected function initAccessor($isForceMaster = false)
    {
        if ($isForceMaster) {
            $this->forceMaster();
        }
        return new MysqlAccessor($this->_table, $this->_configName);
    }

    /**
     * 将 getMapping() 返回的 [property_name=>column_name] 转换到 [column_name=>property_name]
     */
    protected function column2Property()
    {
        $property2ColumnMapping = $this->_property2columnMapping;
        $column2PropertyMapping = [];
        foreach ($property2ColumnMapping as $propertyName => $columnName) {
            $column2PropertyMapping[$columnName] = $propertyName;
        }
        $this->_column2PropertyMapping = $column2PropertyMapping;
    }

    /**
     * 判断属性是否存在对应的数据库字段
     *
     * @param string $property
     * @return bool
     */
    protected function checkFieldExists($property)
    {
        return isset($this->_property2columnMapping[$property]);
    }

    /**
     * 根据对象属性获取对应的数据库字段
     *
     * @param string $property
     * @return string
     */
    protected function getFieldName($property)
    {
        if ($this->checkFieldExists($property)) {
            $this->_property2columnMapping[$property];
        }
        return null;
    }
}